/**
 * This file is part of 1genia trampoline
 * Copyright (C) 2007 1genia (contact@1genia.com)
 *
 * This library is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; version 3 of the License. 
 *
 * This library is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details. 
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; see the file COPYING.TXT.  If not,
 * write to the Free Software Foundation, Inc., 51 Franklin Street,
 * Fifth Floor, Boston, MA 02110-1301, USA. 
 **/
package com.genia.toolbox.web.gwt.form.client.manager;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;

import com.genia.toolbox.web.gwt.basics.client.GwtHelper;
import com.genia.toolbox.web.gwt.basics.client.i18n.GwtI18nMessage;
import com.genia.toolbox.web.gwt.form.client.bean.ExecutionDelayable;
import com.genia.toolbox.web.gwt.form.client.bean.ExecutionDelayableListener;
import com.genia.toolbox.web.gwt.form.client.bean.impl.AbstractExecutionDelayable;
import com.genia.toolbox.web.gwt.form.client.form.Form;
import com.genia.toolbox.web.gwt.form.client.message.Messages;
import com.genia.toolbox.web.gwt.form.client.process.ProcessForm;
import com.genia.toolbox.web.gwt.form.client.validator.ValidatorCallback;
import com.genia.toolbox.web.gwt.form.client.value.DataIdentifier;
import com.genia.toolbox.web.gwt.form.client.value.FormComplexValue;
import com.genia.toolbox.web.gwt.form.client.value.FormSimpleValue;
import com.genia.toolbox.web.gwt.form.client.value.FormValues;
import com.genia.toolbox.web.gwt.form.client.value.impl.FormComplexValueImpl;
import com.genia.toolbox.web.gwt.form.client.value.impl.FormSimpleValueImpl;
import com.genia.toolbox.web.gwt.form.client.visitor.impl.WidgetGeneratorFormVisitor;
import com.genia.toolbox.web.gwt.form.client.widget.item.ComplexItemWidget;
import com.genia.toolbox.web.gwt.form.client.widget.item.ItemWidget;
import com.genia.toolbox.web.gwt.form.client.widget.item.ItemWidgetModifyListener;
import com.genia.toolbox.web.gwt.form.client.widget.item.SimpleItemWidget;
import com.genia.toolbox.web.gwt.form.client.widget.item.impl.DisplayErrorValidatorCallback;
import com.genia.toolbox.web.gwt.form.client.widget.item.impl.ItemWidgetModifyListenerCollection;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.DeferredCommand;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.Panel;

/**
 * Helper class the contain methods to handle forms.
 */
public class FormManager
    extends AbstractExecutionDelayable
    implements ItemWidgetModifyListener
{

  /**
   * an {@link ExecutionDelayableListener} that registers all sub forms and
   * block this form when any of the sub {@link FormManager}s are not
   * initialized.
   */
  private class ExecutionDelayableListenerAggregator
      implements ExecutionDelayableListener
  {

    /**
     * the set of sub {@link FormManager} that are currently not initialized.
     */
    private final Set<ExecutionDelayable> notInitializedFormManagers = new HashSet<ExecutionDelayable>();



    /**
     * record a new sub {@link FormManager} to handle.
     * 
     * @param formManager
     *          the {@link FormManager} to handle
     */
    private void record(FormManager formManager)
    {
      if (!formManager.isInitialized()) {
        notInitializedFormManagers.add(formManager);
        blockInit();
      }
      formManager.addExecutionDelayableListener(this);
    }



    /**
     * release a sub {@link FormManager} that is not needed anymore.
     * 
     * @param formManager
     *          the {@link FormManager} that is not needed
     */
    private void release(FormManager formManager)
    {
      if (notInitializedFormManagers.remove(formManager)) {
        releaseInit();
      }
    }



    /**
     * fired when the source {@link ExecutionDelayable} change state.
     * 
     * @param source
     *          the source that changed state
     * @param sourceIsInited
     *          the new state
     * @see ExecutionDelayableListener#stateChanged(ExecutionDelayable, boolean)
     */
    public void stateChanged(ExecutionDelayable source, boolean sourceIsInited)
    {
      if (sourceIsInited) {
        if (notInitializedFormManagers.remove(source)) {
          releaseInit();
        }
      }
      else {
        if (!notInitializedFormManagers.contains(source)) {
          notInitializedFormManagers.add(source);
          blockInit();
        }
      }
    }

  };

  /**
   * The widgets of this form, associated to their name.
   */
  private transient Map<String, ComplexItemWidget> complexItemWidgets = null;

  /**
   * the identifier of the values begin displayed.
   */
  private transient DataIdentifier dataIdentifier;

  /**
   * {@link Command} that allow to display all validation errors.
   */
  private transient final Command displayAllValidationErrorsCommand = new Command()
  {

    /**
     * Causes the Command to perform its encapsulated behavior.
     * 
     * @see com.google.gwt.user.client.Command#execute()
     */
    public void execute()
    {
      if (simpleItemWidgets != null) {
        for (final SimpleItemWidget simpleItemWidget : new ArrayList<SimpleItemWidget>(simpleItemWidgets.values())) {
          simpleItemWidget.validate(new DisplayErrorValidatorCallback(simpleItemWidget));
        }
      }
      if (complexItemWidgets != null) {
        for (final ComplexItemWidget complexItemWidget : new ArrayList<ComplexItemWidget>(complexItemWidgets.values())) {
          for (final FormManager formManager : complexItemWidget.getFormManagers()) {
            formManager.displayAllValidationErrors();
          }
        }
      }
    }

  };

  /**
   * the {@link ExecutionDelayableListenerAggregator} to use.
   */
  private final ExecutionDelayableListenerAggregator executionDelayableListenerAggregator = new ExecutionDelayableListenerAggregator();

  /**
   * the form to display.
   */
  private transient Form form = null;

  /**
   * the {@link FormController} handling this manager.
   */
  private transient final FormController formController;

  /**
   * the identifier of the form displayed.
   */
  private transient String formIdentifier;

  /**
   * The values of this form.
   */
  private FormValues formValues = null;

  /**
   * the {@link List} of listeners for this widget.
   */
  private final transient ItemWidgetModifyListenerCollection itemWidgetModifyListenerCollection = new ItemWidgetModifyListenerCollection();

  /**
   * variable to prevent a reset to happen if the values have been set in the
   * time when the {@link FormManager} asked the server for the values to reset.
   */
  private transient boolean preventReset = false;

  /**
   * {@link Command} that allows to retrieve the values of the form from the
   * widgets.
   */
  private transient final Command saveFormValuesCommand = new Command()
  {

    /**
     * Causes the Command to perform its encapsulated behavior.
     * 
     * @see com.google.gwt.user.client.Command#execute()
     */
    public void execute()
    {
      for (final Entry<String, SimpleItemWidget> entry : simpleItemWidgets.entrySet()) {
        final String valueName = entry.getKey();
        final SimpleItemWidget simpleItemWidget = entry.getValue();
        FormSimpleValue formSimpleValue = formValues.getFormSimpleValue(valueName);
        if (formSimpleValue == null) {
          FormSimpleValueImpl newFormSimpleValue = new FormSimpleValueImpl();
          newFormSimpleValue.setName(valueName);
          formSimpleValue = newFormSimpleValue;
          formValues.addFormSimpleValue(formSimpleValue);
        }
        formSimpleValue.setValues(simpleItemWidget.getValues());
      }
      for (final Entry<String, ComplexItemWidget> entry : complexItemWidgets.entrySet()) {
        final String valueName = entry.getKey();
        final ComplexItemWidget complexItemWidget = entry.getValue();
        FormComplexValue formComplexValue = formValues.getFormComplexValue(valueName);
        if (formComplexValue == null) {
          FormComplexValueImpl newFormComplexValue = new FormComplexValueImpl();
          newFormComplexValue.setName(valueName);
          formComplexValue = newFormComplexValue;
          formValues.addFormComplexValue(formComplexValue);
        }
        formComplexValue.setValues(complexItemWidget.getValues());
      }
    }

  };

  /**
   * The widgets of this form, associated to their name.
   */
  private transient Map<String, SimpleItemWidget> simpleItemWidgets = null;

  /**
   * the {@link Panel} that will contain the form.
   */
  private final Panel topPanel;



  /**
   * constructor.
   * 
   * @param formController
   *          the {@link FormController} handling this manager
   * @param topPanel
   *          the {@link Panel} that will contain the form
   * @param formIdentifier
   *          the identifier of the form displayed
   * @param dataIdentifier
   *          the identifier of the values begin displayed
   */
  public FormManager(final FormController formController, final Panel topPanel, final String formIdentifier, final DataIdentifier dataIdentifier)
  {

    this.formController = formController;
    this.topPanel = topPanel;
    ProcessForm.addWaitingMessage(topPanel);
    this.formIdentifier = formIdentifier;
    this.dataIdentifier = dataIdentifier;
    if (dataIdentifier == null) {
      throw new NullPointerException("dataIdentifier cannot be null.");
    }
  }



  /**
   * add a listener that is called each time the value of this object change.
   * 
   * @param modifyListener
   *          the callback
   */
  public void addModifyListener(ItemWidgetModifyListener modifyListener)
  {
    itemWidgetModifyListenerCollection.add(modifyListener);
  }



  /**
   * validateAll the item and display errors accordingly.
   */
  public void displayAllValidationErrors()
  {
    execute(displayAllValidationErrorsCommand);
  }



  /**
   * display the current form.
   * 
   * @param callback
   *          the callback that is called once the form is displayed. It is used
   *          to fill the form with the current values.
   */
  public void displayForm(final Command callback)
  {
    if (form == null) {
      ProcessForm.getForm(formIdentifier, dataIdentifier, new AsyncCallback<Form>()
      {

        public void onFailure(final Throwable cause)
        {
          GwtHelper.error(new GwtI18nMessage(Messages.UNABLE_TO_RETRIEVE_FORM_ERROR_KEY, new String[] { cause.toString() }), cause);
        }



        public void onSuccess(final Form form)
        {
          displayForm(form, callback);
        }
      });
    }
    else {
      displayForm(form, callback);
    }
  }



  /**
   * Display a form. The callback {@link Command} must* be calling either
   * {@link FormManager#setFormValues(FormValues)} or
   * {@link FormManager#resetForm()} for the form to finish its initialization.
   * 
   * @param form
   *          the form to display
   * @param callback
   *          the callback that is called once the form is displayed. It is used
   *          to fill the form with the current values
   */
  private void displayForm(final Form form, final Command callback)
  {
    blockInit();
    topPanel.clear();
    final WidgetGeneratorFormVisitor widgetGeneratorFormVisitor = new WidgetGeneratorFormVisitor(this, topPanel);
    form.accept(widgetGeneratorFormVisitor);
    simpleItemWidgets = widgetGeneratorFormVisitor.getSimpleItemWidgets();
    complexItemWidgets = widgetGeneratorFormVisitor.getComplexItemWidgets();

    if (callback != null) {
      callback.execute();
    }
  }



  /**
   * display in the form the values of formValues.
   */
  public void displayFormValues()
  {
    for (final Entry<String, SimpleItemWidget> entry : simpleItemWidgets.entrySet()) {
      final String valueName = entry.getKey();
      final SimpleItemWidget simpleItemWidget = entry.getValue();
      final FormSimpleValue formSimpleValue = formValues.getFormSimpleValue(valueName);
      if (formSimpleValue != null) {
        simpleItemWidget.setValues(formSimpleValue.getValues());
      }
    }
    for (final Entry<String, ComplexItemWidget> entry : complexItemWidgets.entrySet()) {
      final String valueName = entry.getKey();
      final ComplexItemWidget complexItemWidget = entry.getValue();
      final FormComplexValue formComplexValue = formValues.getFormComplexValue(valueName);
      if (formComplexValue != null) {
        complexItemWidget.setValues(formComplexValue.getValues());
      }
    }
    displayAllValidationErrors();
    releaseInit();
  }



  /**
   * getter for the complexItemWidgets property.
   * 
   * @return the complexItemWidgets
   */
  public Map<String, ComplexItemWidget> getComplexItemWidgets()
  {
    return Collections.unmodifiableMap(complexItemWidgets);
  }



  /**
   * getter for the dataIdentifier property.
   * 
   * @return the dataIdentifier
   */
  public DataIdentifier getDataIdentifier()
  {
    return dataIdentifier;
  }



  /**
   * getter for the formController property.
   * 
   * @return the formController
   */
  public FormController getFormController()
  {
    return formController;
  }



  /**
   * getter for the formIdentifier property.
   * 
   * @return the formIdentifier
   */
  public String getFormIdentifier()
  {
    return formIdentifier;
  }



  /**
   * getter for the formValues property.
   * 
   * @return the formValues
   */
  public FormValues getFormValues()
  {
    return formValues;
  }



  /**
   * getter for the simpleItemWidgets property.
   * 
   * @return the simpleItemWidgets
   */
  public Map<String, SimpleItemWidget> getSimpleItemWidgets()
  {
    return Collections.unmodifiableMap(simpleItemWidgets);
  }



  /**
   * returns the value of the item with the given name.
   * 
   * @param name
   *          the item being searched
   * @return a {@link String} representing the value of the item
   */
  public String getValue(final String name)
  {
    final List<String> values = getValues(name);
    if (values == null || values.isEmpty()) {
      return null;
    }
    return values.get(0);
  }



  /**
   * returns the values of the item with the given name.
   * 
   * @param name
   *          the item being searched
   * @return a {@link List} of {@link String} representing the values of the
   *         item
   */
  public List<String> getValues(final String name)
  {
    if (simpleItemWidgets == null) {
      return null;
    }
    saveFormValues();
    final SimpleItemWidget simpleItemWidget = simpleItemWidgets.get(name);
    if (simpleItemWidget == null) {
      return null;
    }
    return simpleItemWidget.getValues();
  }



  /**
   * the method that is call when the value of the item change.
   * 
   * @param itemWidget
   *          the item that changed
   * @see com.genia.toolbox.web.gwt.form.client.widget.item.ItemWidgetModifyListener#onModify(ItemWidget)
   */
  public void onModify(ItemWidget itemWidget)
  {
    itemWidgetModifyListenerCollection.onModify(itemWidget);
  }



  /**
   * record a new sub {@link FormManager}.
   * 
   * @param formManager
   *          the new sub {@link FormManager}
   */
  public void recordNewSubFormManager(FormManager formManager)
  {
    executionDelayableListenerAggregator.record(formManager);
  }



  /**
   * release a sub {@link FormManager}.
   * 
   * @param formManager
   *          the {@link FormManager} to release
   */
  public void releaseSubFormManager(FormManager formManager)
  {
    executionDelayableListenerAggregator.release(formManager);
  }



  /**
   * reset the values of the Form to those re turned by the service for the
   * identifier dataIdentifier.
   * 
   * @param lock
   *          whether to lock the form. This method will ultimately unlock the
   *          form, so it must lock the form if itis called on an unlock form.
   */
  public void resetForm(boolean lock)
  {
    if (lock) {
      blockInit();
    }
    synchronized (this) {
      preventReset = false;
    }
    ProcessForm.getFormService().getIntialValue(formIdentifier, dataIdentifier, new AsyncCallback<FormValues>()
    {

      public void onFailure(final Throwable cause)
      {
        releaseInit();
        GwtHelper.error(new GwtI18nMessage(Messages.UNABLE_TO_RETRIEVE_DATA_ERROR_KEY, new String[] { cause.getMessage() }), cause);
      }



      public void onSuccess(final FormValues formValues)
      {
        synchronized (this) {
          if (preventReset) {
            releaseInit();
            return;
          }
        }
        setFormValues(formValues);
      }

    });
  }



  /**
   * update formValues with the current value of the form.
   */
  public void saveFormValues()
  {
    execute(saveFormValuesCommand);
  }



  /**
   * set the current complex value of the item with the given name.
   * 
   * @param name
   *          the name of the item
   * @param value
   *          a {@link FormValues} representing the new values of the item with
   *          the given name
   */
  public void setComplexValue(final String name, final FormValues value)
  {
    final List<FormValues> values = new ArrayList<FormValues>();
    if (value != null) {
      values.add(value);
    }
    setComplexValues(name, values);
  }



  /**
   * set the current values of the complex item with the given name.
   * 
   * @param name
   *          the name of the complex item
   * @param values
   *          a {@link List} of {@link FormValues} representing the new values
   *          of the item with the given name
   */
  public void setComplexValues(final String name, final List<FormValues> values)
  {
    complexItemWidgets.get(name).setValues(values);
    formValues.getFormComplexValue(name).setValues(values);
  }



  /**
   * Set the formValues properties.
   * 
   * @param formValues
   *          the new {@link FormValues} to set
   */
  public void setFormValues(final FormValues formValues)
  {
    if (formValues.getDataIdentifier() == null) {
      throw new NullPointerException("dataIdentifier cannot be null.");
    }
    synchronized (this) {
      preventReset = true;
    }
    this.formValues = formValues;
    dataIdentifier = formValues.getDataIdentifier();
    formIdentifier = formValues.getFormIdentifier();
    displayFormValues();
  }



  /**
   * set the current value of the item with the given name.
   * 
   * @param name
   *          the name of the item
   * @param value
   *          a {@link String} representing the new values of the item with the
   *          given name
   */
  public void setValue(final String name, final String value)
  {
    final List<String> values = new ArrayList<String>();
    if (value != null) {
      values.add(value);
    }
    setValues(name, values);
  }



  /**
   * set the current values of the item with the given name.
   * 
   * @param name
   *          the name of the item
   * @param values
   *          a {@link List} of {@link String} representing the new values of
   *          the item with the given name
   */
  public void setValues(final String name, final List<String> values)
  {
    simpleItemWidgets.get(name).setValues(values);
    formValues.getFormSimpleValue(name).setValues(values);
  }



  /**
   * send the current values to the server.
   * 
   * @param callback
   *          the {@link Command} called in case of success.
   * @param updateForm
   *          do we need to update the values of the form
   */
  public void submitForm(final Command callback, final boolean updateForm)
  {
    validateAll(new ValidatorCallback()
    {

      public void validationResult(List<GwtI18nMessage> result)
      {
        // We have no more value to validate. Test if all validation was
        // correct
        if (result == null || result.isEmpty()) {
          blockInit();
          // Send the data to the server
          ProcessForm.getFormService().saveForm(getFormValues(), new AsyncCallback<FormValues>()
          {

            public void onFailure(final Throwable cause)
            {
              GwtHelper.error(new GwtI18nMessage(Messages.UNABLE_TO_SAVE_DATA_ERROR_KEY, new String[] { cause.getMessage() }), cause);
              releaseInit();
            }



            public void onSuccess(final FormValues formValues)
            {
              if (updateForm) {
                blockInit();
                setFormValues(formValues);
              }
              if (callback != null) {
                if (updateForm) {
                  execute(callback);
                }
                else {
                  callback.execute();
                }
              }
              if (updateForm) {
                releaseInit();
              }
            }

          });

        }
        else {
          for (GwtI18nMessage gwtI18nMessage : result) {
            GwtHelper.error(gwtI18nMessage);
          }
        }
      }

    });
  }



  /**
   * validate all item of a form, and return the result with a callback.
   * 
   * @param callback
   *          the callback to call when all validation is done
   */
  public void validateAll(final ValidatorCallback callback)
  {
    execute(new Command()
    {
      public void execute()
      {
        saveFormValuesCommand.execute();
        DeferredCommand.addCommand(new FormValidatorCommand(FormManager.this, callback));
      }
    });
  }

}
